File: PC-Phone USB Sync/code/PC-Phone USB Sync--source/pcphoneusbsync.kv

#------------------------------------------------------------------------------
# Part of PC-Phone USB Sync.
# Define the app's GUI, in a way that's integrated with the .py code.
#
# Copyright © 2023-2026 quixotely.com.  All rights reserved.
# License: see ./main.py and ./terms-of-use.txt
#
# Disclaimer: like main.py, this file was not originally meant for 
# publication, so please pardon any rough edges here.  PPUS was a 
# bit of a Kivy starter project, and it still shows in spots.
#
# OVERVIEW
#
# This file uses the Kivy language, which is supposed to separate layout
# from code and use a declarative style but seems a bit too magical
# at times.  The relationship between code here and in the .py is not 
# very well documented, if at all (hint: knowing how classes in the
# .py and .kv are added to the Factory helps; see Frigcal's code docs).
#
# Moreover, the Kivy language doesn't address parts that must still be 
# built in the .py dynamically (e.g., the Main chooser's storage buttons 
# and the entire month screen in Frigcal).  And in hindsight, this code
# may be better manually loaded from a file and passed to the builder 
# in the .py, to avoid an implicit naming convention and some thorny 
# location issues for Kivy executables built with PyInstaller.  
#
# On top of this, Kivy has missing bits (including a text widget that 
# can handle non-trivial text and labels that don't require manual text 
# splits to avoid blackouts); a non-orthogonal sizing/layout scheme
# that smacks too much of CSS (including the endless tweaking - and 
# cursing); and a woeful lack of useful docs in general (expect to 
# scour Kivy's code to grok even fundamental things).  The sum makes
# Kivy seem more a GUI lib construction kit than a finished GUI lib.
#
# OTOH, Kivy does work with the appropriate amount of determination,
# as evidenced by this app and its Frigcal sibling at quixotely.com.
# But you should not expect it to be as friendly as Python's tkinter.
#
# For more Kivy editorial, see also the filechooser workaround ahead.
# It's a great system and very pliable because it's all Python/Cython
# down to the drawing, but it badly needs some finishing tweaks and docs.
# Though unused in this app (see Frigcal), KivyMD improves cosmetics,
# but it also adds a layer to the stack that changes more radically 
# than it should and seems just as thinly documented as Kivy.
#
# Time will hopefully improve the Kivy/KivyMD story.  Beeware/Toga still
# isn't there yet, and life is too short to use Java or Kotlin on Android.
#------------------------------------------------------------------------------




#:kivy 2.1.0

# for refs in py code here
#:import os os
#:import sys sys
#:import Clock kivy.clock.Clock
#:import Window kivy.core.window.Window

# [1.4] do in .py's on_width call back instead, for startup + rotations + multiwindow
# [1.4] now moot - display_cutout abandoned for insets callback, all done in .py
##:import get_height_of_bar android.display_cutout.get_height_of_bar




#------------------------------------------------------------------------------
# GLOBALS
#------------------------------------------------------------------------------


# for phone/pc-specific tweaks (see also top of main.py)

#:set onandroid hasattr(sys, 'getandroidapilevel')
#:set onmacos   sys.platform.startswith('darwin')
#:set onwindows sys.platform.startswith('win')
#:set onlinux   sys.platform.startswith('linux') and not onandroid    # Linux ONLY



# [1.1] use dp(N) instead of N globally, both here and in the .py's code;
# this scales pixels (and widgets) to the host display's density, plus|minus;
# else buttons too small on hi-res displays, and too large on low-res displays;
# these may be overriden by size_hint_y %s, and/or sizing to self.texture_size;
# also use kivy.metrics.dp(N) instead of N in .py for dynamically built widgets;
# see also the related global font size's sp() scaling applied in the .py;
# all per https://kivy.org/doc/stable/api-kivy.metrics.html (see also cm());


# use density-scaled pixels, textheight no longer used [1.1]

#:set buttonheight dp(40)
#:set tallbuttonheight buttonheight


## absolute pixels: not ideal, but they work (if size_hint_y=None)
##:set textheight   90
##:set buttonheight 80

## phone increment; now moot [1.1]
##:set buttonheight (buttonheight + (4 if onandroid else 0))

## easier taps of crucials; now moot [1.1]
##:set tallbuttonheight (buttonheight + 16 if onandroid else buttonheight)




#------------------------------------------------------------------------------
# CLASSES
#------------------------------------------------------------------------------


# CAUTION: dynamic class names cannot be same as a Kivy class name.
# This is why an <ActionButton@Button> here silently never worked...
# Classes coded here and in the .py wind up in the Kivy Factory object.




# Folder paths, extended ahead

<PathDisplay@TextInput>:
    multiline: False                         # one-line editable text field
    foreground_color: 'black'
    background_color: 'white'

    # fix text clipping for higher user fontsize settings, and rightcrunch/curves

    size_hint_y: None
    height: self.minimum_height              # lines height, includes padding

    # [1.5] padding is weirdly brittle - don't change, else breaks valign of pathnames!

    #padding: [16, 6, 16, 6]                # [l, t, r, b], default [6]*4 (pixels)
    padding: [dp(7), dp(3), dp(7), dp(3)]    # scale to screen density, +t/b -l/r [1.1]

    # [1.5] valign properly, per current size: Main tab more usable in slab landscape

    padding_y: (self.height - self.line_height) / 2    # l+r still from padding (deprecated)

    # [1.1] do grab+move scrolling on PCs (phones already do swipe scroll); 
    # holding longer selects text instead; a ScrollView might help too (tbd,
    # but horizontal text scrolls are odd - see .py enable_logs_text_hscroll());

    scroll_from_swipe: True    # default is False for PCs, True for phones

    # always show start of text on left edge of widget, until scrolled;
    # now done for initial open in main.py's startup_gui_and_config_tweaks(),
    # via text.cursor = (0, 0); halign and scroll_x here had no effect (why?);

    #valign: 'middle'    # moot? ([1.5]: yes, and nonexistent in TI - use padding_y)
    #halign: 'left'      # no effect?
    #scroll_x: 0         # no effect?




# Multiple inheritance
# Yes, it works, and uses mro (not simple dflr) if it's mapped to Python classes (tbd)

<PathDisplayNoSelect@TextInputNoSelect+PathDisplay>




# Global fontsize for all widgets
# Yes, it works, if bound to app.dyn_font_size, not self|root (but KivyMD may override)

<Widget>:
    font_size: app.dynamic_font_size    # applied to every Widget subclass instance (!)




# Custom labels

<SectionLabel@Label>:                   # above sections in tabs
    halign: 'center'
    text_size: root.width, None         # this is used, but may have no effect at all...
    size: self.texture_size
    color: 'cyan'
    font_name: 'Roboto-BoldItalic'      # for the kids

<LineLabel@Label>:                      # to the right of other widgets
    text_size: self.size
    halign: 'left'
    size_hint_y: 1
    valign: 'middle'                    # valign works on Label,  but not TextInput
    # padding: [32, 32]                 # [h, v] - but v made moot by size_hint_y!
    padding: [dp(16), dp(16)]           # scale to screen density [1.1]




# Color schemes - could be user configs someday (tbd, tmi?)

<PathButton@Button>
    background_color: 'blue'

<ActivityButton@Button>
    background_color: 'green'

<StorageButton@Button>
    background_color: 'red'    # not used here: made in .py




# [1.5] Redo dialog+tab footer buttons to have a uniform fixed size
# and not change with window resizes - else may become too small.
# This replaces a jumble of BoxLayouts that diverged over time.


# use density-scaled pixels for varying displays (defined above)
##:set buttonheight dp(40)

<DialogButtonsRows1@GridLayout>:        # one-row button grid, fixed size
    rows: 1
    size_hint_y: None
    height: buttonheight                # and add N DialogButton, cols = N

<DialogButtonsRows2@GridLayout>:        # two-row button grid, fixed size
    rows: 2
    size_hint_y: None
    height: buttonheight * 2            # and add N DialogButton, cols = ceil(N / 2)

<DialogButton@Button>
    background_color: 'navy'




# [1.5] Enable markup in text displays, and split large text across N 
# labels for Help and About - a workaround for Label text-size limits,
# and required to avoid blank/blacked-out labels for nontrivial text.
# Unlike KivyMD, Kivy Label has no background color sans Canvas ops.
# Text requires massive amounts of memory: see Recycleview fail ahead.


<SplitTextBoxLayout@BoxLayout>:

    # [1.5] Custom container for Label parts of Help and About screens. 
    # The .py adds N LabelTextDisplay instances based on text size.

    orientation: 'vertical'              # stack children on top of each other
    size_hint_y: None                    # plain Kivy (not KivyMD) version
    height: self.minimum_height
    #adaptive_height: True               # KivyMD: adjust height per childrens' content


<BackgroundColor@Widget>:

    # [1.5] UNUSED experiment that was abandoned before it worked.
    # Set backround_color and color as construction arguments.
    # Mix in to another widget class to enable bg colorization;
    # canvas.before ops are run before widget is drawn on canvas.

    # PUNT: did not work as coded, no time for struggles, and the
    # Kivy default dark-gray background is better for consistency.

    background_color: 1, 1, 1, 1         # default if not overridden by subclass
    canvas.before:
        Color:
            rgba: root.background_color
        Rectangle:
            size: self.size
            pos: self.pos


<LabelTextDisplay@Label>:    # punt: "+BackgroundColor>:"

    # [1.5] For showing text in labels with Kivy markup.  This is used as
    # is in dialogs, and larger Help/About text is spread across N of these
    # in the .py, else nontrivially sized text may be blacked out (empty).
    # Kivy/KivyMD TextInput/Field handle larger text, but have no markup.
    # KivyMD: must bind font_size after create in .py else auto-set per MD.

    # Now also used for dialogs like info_message() to enable colors and 
    # custom [H] headers implemented by this app.  This sets bg to default
    # gray, and text is not split for dialogs: assumed to be short enough.

    # This replaces prior highly-customized TextInputNoSelect appearances.
    # Note that Kivy Label wraps if text_size or embedded \n characters; per
    # its code, KivyMD MDLabel autowraps if no adaptive_size or adaptive_height.

    # NIT: Label uses different line-wrap rules than Text - it splits on just 
    # spaces and not periods.  This works fine for prose but can be subpar for
    # pathnames.  Label splits use split_str in kivy.core.text's LabelBase, 
    # which defaults to ' ' and can be changed, but Text doesn't split on / or
    # \ either and uses . unevenly, and this is a substantial can of worms...

    markup: True                         # use '[]' for bold, italic, under, color, etc.

    text_size: self.width, None          # enables word wrapping
    size_hint_y: None                    # use plain kivy version, no kivymd adaptive_*
    size: self.texture_size              # no min_height (and why is sizing so wonky?)
   
    halign: 'left'
    padding: [dp(10), 0]                 # just [h, v] in Kivy 2.1.0, not [l, t, r, b], was abs 8

    # these work only in Text...         # harmless but pointless in Label: kivy silently ignores! 
    # multiline: True                    # for Text, enable N line plus line wraps on words
    # do_wrap: True                      # tip: Label also splits text on embedded \n

    # oddments...
    # allow_copy: False                  # irrelevant for Label, for TextInput = no edit bubbles
    # theme_text_color: 'Primary'        # kivymd only: also foreground_color, background_color

    # the usual sizing song and dance...
    # adaptive_height: True              # kivymd only: enables scrolling = [size_hint_y: None] + [height: self.minimum_height]
    # size_hint: 1, 1                    # relative to parent size: fill available space, default
    # size_hint: (None, None)            # enable provided size (and its many variants)
    # height: self.minimum_height        # for TextInput, not label (inconsistently)
    # height: self.texture_size[1]       # non-orthogonal like CSS (same persistence required)



# CATCH: Kivy 2.1.0 allows just two values for Label padding, [h, v], so we
# must handle top xor bottom padding specially.  One-label cases (dialogs) 
# can pad both, but N-label split-text cases (Help/About) must add spacers 
# above and below the labels container.  Kivy 2.3.1 allows [l, t, r, b] 
# for padding, which allows Frigcal to handle Help/About padding in .py 
# code, but 2.3.1 is too risky to use for PPUS given PPUS's many brittle 
# workarounds for Kivy bugs: disable in the .py, and pad here instead.
# See CATCH in the .py's set_split_label_text() for expanded coverage.

# Help/About
<VerticalSpacer10@Label>:
    size_hint_y: None     
    height: dp(10)        # vertical spacer, 10 density-scaled pixels high

# dialogs
<SpacedLabelTextDisplay@LabelTextDisplay>:
    padding_y: dp(10)     # pad both top and bottom in kivy 2.1.0
    padding_x: dp(8.0)    # match dialog header's indent, dialog already padded




# Now defined in .py for conditional color
# <ConfigCheckbox@CheckBox>
#     # [r, g, b, a] for image tinting - too dim on macos only
#     color: [1, 3, 5, 4] if sys.platform.startswith('darwin') else [1, 1, 1, 1]
#     color: self.check_box_color  # handle in .py, auto-update on changes




#------------------------------------------------------------------------------
# ROOT WIDGET
#------------------------------------------------------------------------------


Main:

    # Root widget (instance), maps to .py's class Main via Factory

    # available in .py
    # run_output: runoutput

    # ref as this or self.ids.<id> or self.ids['id']
    from_Path: frompath
    to_path: topath


    # [1.4] negate android 15 edge-to-edge display by padding window for insets
    # now done in .py's on_width calback here for startup + roatations + multiwindow
    # padding: 0, get_height_of_bar('status'), 0, get_height_of_bar('navigation');
    # PUNT: failed for small-screen landscape where navbar is on left or right;
    # see .py for later insets-based solution that is a more complete solution;

    ##on_width: root.neuter_android15_edge_to_edge()
    ##on_width: root.force_redraw()


    TabbedPanel:
        # size_hint: .5, .5
        # pos_hint: {'center_x': .5, 'center_y': .5}

        id: toptabs
        do_default_tab: False
        on_current_tab: root.on_tab_switch()

        # Failed [1.1]: force redraw for Android phone rotations;
        # neither this nor root.canvas.ask_update() worked; may be
        # an sdl2 glitch: see docs in .py enable_logs_text_hscroll()
        # and later on_start() tries - display size events are being
        # dropped by SDL2 or Kivy, or botched by Samsung Android.
        #
        # on_width: 
        #    (print('redraw'), 
        #    (Clock.schedule_once(lambda dt: (print('layout'), self._trigger_layout())), 1.0)
        #    if onandroid else None) 




#------------------------------------------------------------------------------
# MAIN TAB
#------------------------------------------------------------------------------


        TabbedPanelItem:
            id: maintab
            text: 'Main'

            BoxLayout:                                 # layout container required in each tab
                orientation: 'vertical'


                SectionLabel:
                    size_hint_y: .10
                    text: 'Choose Content Folders'

                GridLayout:
                    cols: 2
                    size_hint_y: .15
                    # cols_minimum: {0: dp(20)}
                    # padding: [10, 0, 10, 0]          # [l, t, r, b]
                    padding: [dp(0), 0, dp(5), 0]      # drop pads, scale to density [1.1]

                    
                    PathButton:
                        size_hint_x: 0.40
                        text: 'FROM'
                        on_release: root.do_main_path('FROM', 'frompath')

                    PathDisplay:
                        size_hint_y: 1
                        id: frompath
                        text: 'This comes from settings default or save...'
                        font_name: 'DejaVuSans'


                    PathButton:
                        size_hint_x: 0.40
                        text: 'TO'
                        on_release: root.do_main_path('TO', 'topath')

                    PathDisplay:
                        size_hint_y: 1
                        id: topath
                        text: 'This comes from settings default or save...'
                        font_name: 'DejaVuSans'

                        on_text:
                            # [1.1] Never reenable UNDO if any action is in 
                            # progress, else user could launch a parallel UNDO!
                            # But clear undo_verboten so enabled on script exit,
                            # and enable UNDO now iff no action running now.
                            # This fires for both manual and chooser TO mods.

                            root.undo_verboten = False;
                            if not root.script_running: undobutton.disabled = False


                SectionLabel:
                    size_hint_y: 0.10
                    text: 'Start Action'

                GridLayout:
                    cols: 2
                    size_hint_y: 0.50
                    # cols_minimum: {0: 0.25, 1: 0.75}
                    # padding: [10, 0, 10, 0]           # [l, t, r, b]
                    padding: [dp(0), 0, dp(0), 0]       # drop pads, scale to density [1.1]


                    ActivityButton:
                        size_hint_x: 0.40
                        id: syncbutton
                        text: 'SYNC'
                        on_release: root.do_sync(frompath.text, topath.text)

                    LineLabel:
                        text: 'Make TO the same as FROM'
                        # text: 'Propagate changes in FROM to TO'


                    ActivityButton:
                        size_hint_x: 0.40
                        id: showbutton
                        text: 'SHOW'
                        on_release: root.do_show(frompath.text, topath.text)
 
                    LineLabel:
                        text: 'Report FROM/TO differences only'


                    ActivityButton:
                        size_hint_x: 0.40
                        id: undobutton
                        text: 'UNDO'
                        on_release: root.do_undo(topath.text)

                    LineLabel:
                        text: 'Roll back TO\'s most recent SYNC'


                    ActivityButton:
                        size_hint_x: 0.40
                        id: copybutton
                        text: 'COPY'
                        on_release: root.do_copy(frompath.text, topath.text)

                    LineLabel:
                        text: 'Make a full copy of FROM in TO'


                    ActivityButton:
                        size_hint_x: 0.40
                        id: diffbutton
                        text: 'DIFF'
                        on_release: root.do_diff(frompath.text, topath.text)

                    LineLabel:
                        text: 'Compare FROM to TO byte for byte'


                    ActivityButton:
                        size_hint_x: 0.40
                        id: namebutton
                        text: 'NAME'
                        on_release: root.do_name(frompath.text)

                    LineLabel:
                        id: namelabel
                        text: 'Make filenames portable in FROM'


                SectionLabel:
                    size_hint_y: 0.10
                    id: statuslabel
                    text: 'Action Status'

                Image:
                    size_hint_y: 0.20
                    id: statusimg
                    source: 'usbsync-anim.gif'
                    anim_delay: -1                # frames/sec, -1=stop, 0.20=go
                    anim_loop: 0                  # reps till stop, 0=keep looping  




#------------------------------------------------------------------------------
# LOGS TAB
#------------------------------------------------------------------------------


        TabbedPanelItem:
            text: 'Logs'
            id: logstab

            BoxLayout:
                orientation: 'vertical'


                SectionLabel:
                    id: logslabel
                    text: 'View Run Logfiles'
                    #size_hint_y: 0.05               # [1.5] size+padding for splitter
                    size_hint_y: None
                    size: self.texture_size
                    padding_y: dp(7)


                Label:
                    id: logspath
                    text: root.logfile_path
                    text_size: root.width, None
                    halign: 'center'
                    #size_hint_y: 0.05               # [1.5] size+padding for splitter
                    size_hint_y: None
                    size: self.texture_size
                    padding_y: dp(6)

                    # else small/big display/font wraps to a line 2, which is
                    # partly vclipped no reason to scroll this: rarely sortened, 
                    # it's info only (+in docs), and can open|explore to check;
                    # this shortens on 'right', because it's a path, and always
                    # ends in long appname; could hack Main's chooser popup to do
                    # same in list view, but its 'center' is useful for basenames;

                    shorten: True
                    shorten_from: 'right'    # default 'center' obscures worse


                # using a kivy filechooser here was a major pain in the app:
                # autoselect was elusive, prior selections were not cleared after 
                # _update_files() (autoselect or not), icon-view didn't show full 
                # filename, and popup requires pointless taps; punt and go custom

                AnchorLayout:
                    size_hint_y: 0.30
                    anchor_x: 'center'    # center scroll+filelist in window 

                    ScrollView:
                        # pos_hint: {'center_x': .5}
                        # width: picklogitems.width
                        size_hint_x: None            # which is like (None, 1)

                        id: picklogscroll
                        do_scroll_x: True
                        do_scroll_y: True
                        effect_cls: 'ScrollEffect'    # don't overscroll/snapback

                        # one of the more subtle bits here: this ensures that 
                        # the filelist scrolls horizontally if the window is too
                        # narrow to display it, by listening for changes in the
                        # root's width; this also ensures hscrolls if the filelist
                        # doesn't fit the window due to large fontsize settings (on
                        # Apply, startup, or restore), because it's listening to both
                        # window size and filelist size; the filelist normally fits 
                        # the display and the scrollbar is set the same width - this
                        # is just for pathologically narrow displays or large fonts;

                        width: min(picklogitems.minimum_width, root.width)

                        BoxLayout:
                            id: picklogitems
                            orientation: 'vertical'              # was grid - cols: 1

                            # use less padding on PCs, to match phones [1.1]
                            spacing: 
                                # scale to density, not platform [1.1]
                                dp(2.5)

                                # an initial resizing, superseded [1.1]
                                # (8 if onandroid
                                # else (5 if onmacos else 4))    # was grid - spacing: (0, 8) # (h, v)                        

                            # do in .py for dynamic creation
                            # size_hint: (None, None)
                            # width:  self.minimum_width
                            # height: self.minimum_height
 

                # [1.5] Make Logs' middle green action-buttons row fixed sized too?
                # PROS: avoid growth as window expands so add as much log-viewer space 
                # as possible.  Won't shrink on window contract, but too small unusable.
                #
                # CONS: this reveals only another line or two, because the logfiles 
                # list at the top also expands; it differs from the Main and Config
                # tabs, whose buttons do grow+shrink with window; and Android landscape
                # and split/popup multi-window modes argue for allowing row to shrink.
                # Logs' logfiles-list cells do not grow+shrink, but they're unmoddable.
                #
                # USED: this became fixed in the end, for the new Splitter - see below.
                # ALSO: changed WATCH to trigger by on_press instead of on_release, else
                # no callback sent and button is stuck on and requirestap to clear if 
                # swipe over button - which happened in 1.4 too but is much more likely 
                # in 1.5 because the slider is just below WATCH.  Other buttons still 
                # require a release over the button to trigger: okay - not toggles.           

                #BoxLayout:
                #    size_hint_y: 0.08
                #    orientation: 'horizontal'

                DialogButtonsRows1:      # [1.5] fixed-size row, not % relative (for splitter)
                    id: logsactionsrow

                    ActivityButton:
                        text: 'TAIL'
                        id: logfiletail
                        on_release: root.do_logs_tail(picklogitems)

                    ToggleButton:
                        text: 'WATCH'
                        id: logfilewatch
                        background_color: 'green'
                        on_press: root.do_logs_watch(picklogitems)    # [1.5] not on_release, else stuck on if swipe

                    ActivityButton:
                        text: 'OPEN'
                        id: logfileopen
                        on_release: root.do_logs_open(picklogitems)

                    ActivityButton:
                        text: 'EXPLORE'
                        id: logfileexplore
                        on_release: root.do_logs_explore()


                # [1.5] Wrap file-view area in a Splitter so users can resize it.
                # This may avoid the extra steps of OPEN in some cases, and allows 
                # more filenames to be viewed without scrolling.  Both file-view and 
                # filename areas still scroll as before, but the green action-buttons 
                # row is now fixed size so only the other two respond to the splitter,
                # and space has been hardcoded around the top label and filepath.  

                # To avoid running widgets over top tabs or cruching the label/path, 
                # must calculate and limit max_size of file-view area from Window.
                # On Android, max_size also requires deducting edge-to-edge padding; 
                # on Windows+Linux, it uses less fudge so it fully covers files list
                # app.android_e2e_window_height_pad: property, so mods update max_size;

                # Can split long property values with indent + '\', despite Kivy docs!
                # Python (a if x else b if y else c) == (a if x else (b if y else c)).
                # The splitter bar supports both drags and double taps for resizes.
                # Its default pt(10) (a.k.a. '10pt' in the source) was nearly unusable 
                # on Linux (only), and just a hair too narrow on Windows touch screens.

                Splitter:
                    id: logssplitter
                    sizable_from: 'top'                # resizable from the top side of this widget

                    # minimum height: smallest that logview can be at display bottom
                    min_size: dp(96)

                    # maximum height: else top tabs overlayed or label+path crunched                 
                    #max_size: Window.height * .80  

                    # more accurate: stop at bottom of label+path, deducts e-2-e padding and fudge
                    max_size: 
                        Window.height - \
                        (toptabs.tab_height + \
                        logsactionsrow.height + logslabel.height + logspath.height + \
                        app.android_e2e_window_height_pad + \
                        (8 if onwindows else 6 if onlinux else 12))

                    size_hint_y: 0.50                  # initial percent of window
                    rescale_with_parent: True          # retain parent % when parent resized (moot w/max?)

                    # size of the divider bar itself, defaults to '10pt' (i.e., pt(10))               
                    strip_size: 
                        pt(14) if onlinux else \
                        pt(10) if onandroid else \
                        pt(12)    # default: Linux unusable, Windows+macOS narrowish, Android ok

                    ScrollView:                        # only one child allowed in splitter
                        # because TextInput itself would not scroll horizontally 
                        id: logfilescroll

                        #size_hint_y: 0.50
                        do_scroll_x: True
                        do_scroll_y: True
                        effect_cls: 'ScrollEffect'     # don't overscroll/snapback

                        # Update [1.1]: there's no need to do the following
                        # on _both_ height and width - vscroll is never lost, 
                        # and just width catches both Android device rotations
                        # and PC width-only resizes; but it is required on width
                        # for both android and PCs, else hscroll is lost on all

                        # else hscroll lost on rotation (kivy bug)
                        # on_height: root.enable_logs_text_hscroll()    # extraneous [1.1]

                        # else hscroll lost on width-only PC resizes
                        on_width: root.enable_logs_text_hscroll()       # necessary+sufficient

                        TextInputNoSelect:
                            id: logfiletext
                            text: 'Logfile content...'
                            multiline: True
                            do_wrap: False
                            background_color: logsbackgroundcolor.text
                            foreground_color: logsforegroundcolor.text

                            # and selection+handles+bubbles disabled in .py
                            # [1.5] see also kivy's use_handles, use_bubbles
                            readonly: True    # else keyboard still covers (iff font?)

                            # default Roboto-Regular
                            # also DejaVuSans, Roboto-{Bold, Italic, BoldItalic}

                            font_name: 'RobotoMono-Regular' 

                            # to enable scrolling
                            size_hint: (None, None)
                            width: logfilescroll.width
                            height: max(self.minimum_height, logfilescroll.height)

                            # on each text change? NO - not required here
                            # run this only on logfile load + scroll height
                            # on_text: root.enable_logs_text_hscroll()

                            # unlike others, this is hscrolled and not wrapped
                            # CAUTION: the .py's hscroll size calc includes this padding
                            # padding: [24, 24]          # [h, v], mind the screen curves
                            padding: [dp(12), dp(12)]    # scale to screen density [1.1] 




#------------------------------------------------------------------------------
# CONFIG TAB
#------------------------------------------------------------------------------


        TabbedPanelItem:
            id: configtab
            text: 'Config'

            # [1.5] Wrap the whole setings area in a ScrollView for very
            # small displays?  NO: could not get this to scroll as coded,
            # and it fits most contexts as is, except for narrow-phones
            # landscape, which is likely unusable in general.  The Main 
            # tab is also unscrolled, because it fits every practical size.
            #
            # UPDATE: changes to properly vertically align the text inputs
            # in Config also made this tab much more usable in landscape 
            # mode on slab phones - which was a primary driver for scrolls.
            # This similarly improves the Main tab's pathnames on slabs.

            BoxLayout:
                orientation: 'vertical'


                # Start setting-widgets area

                # ScrollView:       # [1.5] PUNT
                #     BoxLayout:


                # Checkbutton + Label

                GridLayout:
                    size_hint_y: 0.40
                    cols: 2
                    # padding: [16, 16, 0, 0]        # [l, t, r, b]
                    padding: [dp(8), dp(8), 0, 0]    # scale to screen density [1.1]


                    # [1.4] reordered to group now-three chooser settings 

                    ConfigCheckbox:
                        size_hint_x: 0.20
                        id: backupscheckbox
                        active: True
                        on_press: root.on_backups_clicked(self.active)     # not on_touch 

                    LineLabel:
                        size_hint_x: 0.80
                        text: 'Back up SYNC changes in TO for UNDO?'       # [1.4] shorter


                    ConfigCheckbox:
                        size_hint_x: 0.20
                        id: skipcruftscheckbox
                        active: True

                    LineLabel:
                        size_hint_x: 0.80
                        text: 'Skip one-platform items in FROM and TO?'    # [1.4] shorter


                    ConfigCheckbox:
                        size_hint_x: 0.20
                        id: runactionasservice   # force off in .py for Android 8
                        active: True

                    LineLabel:
                        size_hint_x: 0.80
                        id: runactionasservicelabel
                        text: 'Run Main actions as services on Android?'

            
                    # [1.1] allow users to toggle keep-screen-on;
                    # for both Android (timeout) and PCs (sceensaver);
                    # also reordered toggles and tweaked sizes for fit;

                    ConfigCheckbox:
                        size_hint_x: 0.20
                        id: keepscreenon
                        active: True
                        on_press: root.on_keepscreenon_clicked(self.active)
                    
                    LineLabel:
                        size_hint_x: 0.80
                        text: 'Keep the screen on while using this app?'


                    # [1.4] Togggle hiddens view via filechooser.show_hidden.
                    # The newly opened popup's filechooser show_hidden is set in 
                    # .py from this Main Config-tab setting, which is persisted.
                    # filechooser's is_hidden is already guarded against hangs.

                    ConfigCheckbox:
                        size_hint_x: 0.20
                        id: showhiddens
                        active: False
                    
                    LineLabel:
                        size_hint_x: 0.80
                        text: 'Show hidden folders in choosers?'


                    ConfigCheckbox:
                        size_hint_x: 0.20
                        id: rootfolderselections
                        active: False
                    
                    LineLabel:
                        size_hint_x: 0.80
                        text: 'Show root folder in choosers if possible?'


                    ConfigCheckbox:
                        size_hint_x: 0.20
                        id: appfolderselections
                        active: True

                    LineLabel:
                        size_hint_x: 0.80
                        id: appfolderselectionslabel
                        text: 'Show app folder in choosers on Android?'


                # Input + Label

                GridLayout:
                    size_hint_y: 0.25
                    cols: 2
                    # padding: [16, 16, 0, 0]          # [l, t, r, b]
                    padding: [dp(8), dp(8), 0, 0]      # scale to screen density [1.1]


                    TextInput:
                        size_hint_x: 0.20
                        id: maxnumbackups
                        text: '25'

                        # [1.5] filter limits to 0-9 but allows '' (crashes): default in .py
                        input_filter: 'int'
                        on_focus: if not self.focus: root.on_int_input_defocus('maxnumbackups')

                        multiline: False
                        #valign: 'middle'                 # moot, nonexistent in Text

                        # [1.5] valign properly, per current size 
                        padding_y: (self.height - self.line_height) / 2
                        padding_x: dp(8)

                        #padding: [16, 16, 16, 0]         # [l, t, r, b], default [6]*4 (pixels)
                        #padding: [dp(8), 0, dp(8), 0]    # scaled per density [1.1]

                        #height: self.minimum_height      # lines height, includes padding
                        #size_hint_y: 1

                    LineLabel:
                        size_hint_x: 0.80
                        text: 'Keep up to this many SYNC backups'


                    TextInput:
                        size_hint_x: 0.20
                        id: maxnumlogfiles
                        text: '30'

                        # [1.5] filter limits to 0-9 but allows '' (crashes): default in .py
                        input_filter: 'int'
                        on_focus: if not self.focus: root.on_int_input_defocus('maxnumlogfiles')

                        multiline: False
                        #valign: 'middle'    # moot, nonexistent in Text

                        # [1.5] valign properly, per current size 
                        padding_y: (self.height - self.line_height) / 2
                        padding_x: dp(8)

                        #padding: [16, 16, 16, 0]
                        #padding: [dp(8), 0, dp(8), 0]   # scaled per density [1.1]

                    LineLabel:
                        size_hint_x: 0.80
                        text: 'Keep up to this many run logfiles'


                    TextInput:
                        size_hint_x: 0.20
                        id: maxlogstailsize
                        text: str(10 * 1024)

                        # [1.5] filter limits to 0-9+',' but allows '' (crashes): default in .py
                        input_filter: lambda s, u: s if s in '0123456789,' else ''     
                        on_focus: if not self.focus: root.on_int_input_defocus('maxlogstailsize')

                        multiline: False
                        #valign: 'middle'    # moot, nonexistent in Text

                        # [1.5] valign properly, per current size 
                        padding_y: (self.height - self.line_height) / 2
                        padding_x: dp(8)

                        #padding: [16, 16, 16, 0]
                        #padding: [dp(8), 0, dp(8), 0]   # scaled per density [1.1]

                    LineLabel:
                        size_hint_x: 0.80
                        text: 'Show up to this many bytes in Logs TAIL'


                # Input + Button + Label

                GridLayout:
                    size_hint_y: 0.35
                    cols: 3
                    # padding: [16, 16, 0, 0]        # [l, t, r, b]
                    padding: [dp(8), dp(8), 0, 0]    # scaled per density [1.1]


                    TextInputNoSelect:
                        size_hint_x: 0.20
                        id: logsbackgroundcolor
                        text: 'black'
                        readonly: True
                        disabled: True

                        multiline: False
                        #valign: 'middle'    # moot, nonexistent in Text

                        # [1.5] valign properly, per current size 
                        padding_y: (self.height - self.line_height) / 2
                        padding_x: dp(8)

                        #padding: [16, 16, 16, 0]
                        #padding: [dp(8), dp(8), dp(8), 0]    # scaled per density [1.1]

                    ActivityButton:
                        size_hint_x: 0.20
                        text: 'Pick'
                        on_release: root.pickcolor(root.do_bg_color, 'Background')

                    LineLabel:
                        size_hint_x: 0.60
                        text: 'Logs-display background color'


                    TextInputNoSelect:
                        size_hint_x: 0.20
                        id: logsforegroundcolor
                        text: 'white'
                        readonly: True
                        disabled: True

                        multiline: False
                        #valign: 'middle'    # moot, nonexistent in Text

                        # [1.5] valign properly, per current size 
                        padding_y: (self.height - self.line_height) / 2
                        padding_x: dp(8)

                        #padding: [16, 16, 16, 0]
                        #padding: [dp(8), dp(8), dp(8), 0]    # scaled per density [1.1]

                    ActivityButton:
                        size_hint_x: 0.20
                        text: 'Pick'
                        on_release: root.pickcolor(root.do_fg_color, 'Foreground')

                    LineLabel:
                        size_hint_x: 0.60
                        text: 'Logs-display foreground color'


                    TextInput:
                        size_hint_x: 0.20
                        id: globalfontsize
                        text: '40'

                        # [1.5] filter limits to 0-9 but allows '' (crashes): default in .py
                        input_filter: 'int'
                        on_focus: if not self.focus: root.on_int_input_defocus('globalfontsize')

                        multiline: False
                        #valign: 'middle'    # moot, nonexistent in Text

                        # [1.5] valign properly, per current size 
                        padding_y: (self.height - self.line_height) / 2
                        padding_x: dp(8)

                        #padding: [16, 16, 16, 0]
                        #padding: [dp(8), dp(8), dp(8), 0]    # scaled per density [1.1]

                    ActivityButton:
                        size_hint_x: 0.20
                        text: 'Apply'
                        on_release: root.set_font_size(globalfontsize.text)   # [1.5] .py traps ''

                    LineLabel:
                        size_hint_x: 0.60
                        text: 'Global font size in pixels'


                    # Radio button + Radio button + Label

                    # chooser popup uses these on open, and sets them on changes;
                    # setting in config doesn't impact chooser until popup open;

                    # one might expect that pressing a radio-group button selects it 
                    # and clears the other, but one would be wrong in kivy - without 
                    # the on_press, it's possible to have none selected in a group.
                    # UPDATE: the scantly documented allow_no_selection does the 
                    # trick, but weirdly must be declared for each item in the group.
                    # also now used in Logs' file chooser too, but in .py, not .kv.

                    ToggleButton:
                        size_hint_x: 0.20
                        id: mainchoosericon
                        text:  'Icon'
                        group: 'mainchooserstyle'
                        state: 'down'
                        # on_press: self.state = 'down'; mainchooserlist.state = 'normal'
                        allow_no_selection: False

                    ToggleButton:
                        size_hint_x: 0.20
                        id: mainchooserlist
                        text:  'List'
                        group: 'mainchooserstyle'
                        # on_press: self.state = 'down'; mainchoosericon.state = 'normal'
                        allow_no_selection: False

                    LineLabel:
                        size_hint_x: 0.60
                        text: 'Style of Main folder chooser'


                # End setting-widgets area

                # Tab remembers, but saves for non-run items here only on 
                # manual Save tap, not auto on Main run.  Save here saves  
                # just this tab's settings, Main run saves Main paths only.

                #BoxLayout:
                #    size_hint_y: 0.10
                #    # padding: [0, 16, 0, 0]     # [l, t, r, b]
                #    padding: [0, dp(8), 0, 0]    # scaled per density [1.1]
                #    height: buttonheight
                #    orientation: 'horizontal'

                Label: 
                    size_hint: None, None
                    height: dp(8)                  # custom top space above buttons

                DialogButtonsRows1:                # [1.5] uniform fixed size

                    DialogButton:
                        text: 'Save Changes'
                        on_release: root.save_persisted_settings_Config()

                    DialogButton:
                        text: 'Restore Defaults'
                        on_release: root.reset_persisted_settings_Config()




#------------------------------------------------------------------------------
# HELP TAB
#------------------------------------------------------------------------------


        TabbedPanelItem:
            id: helptab
            text: 'Help'

            BoxLayout:
                orientation: 'vertical'

                #SectionLabel:
                #    size_hint_y: 0.10
                #    text: 'Usage Essentials'


                ScrollView:
                    # because TextInput itself would not scroll horizontally 
                    size_hint_y: 0.95             # [1.5] was 0.70
                    size_hint_x: 1.0
                    id: helptextscroll
                    do_scroll_x: False
                    do_scroll_y: True
                    effect_cls: 'ScrollEffect'    # don't overscroll/snapback


                    # [1.5] use multiple Labels for text markup
                    # Text fields don't do markup, but Labels display as empty
                    # space for any trivially-large text.  To work around, split 
                    # text into N parts and span across multiple Labels in .py. 
                    # Labels allow markup: bold, italics, colors, [H] headers.

                    # CATCH: unlike Kivy 2.31, 2.1.0 Label padding is always just
                    # two values, [h v], so must pad manually at top and bottom of 
                    # the split labels box.  See <LabelTextDisplay> for more info.

                    BoxLayout:
                        orientation: 'vertical'
                        size_hint_y: None
                        height: self.minimum_height

                        VerticalSpacer10:       # add padding at top: kivy 2.1 padding is 2 vals

                        SplitTextBoxLayout:     # main.py adds N LabelTextDisplay for split text
                            id: helptextbox     # this precludes a common Help/About class

                        VerticalSpacer10:       # add padding at bottom: kivy 2.1 padding 2 vals


                    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                    # [1.5] prior version - retain temp
                    # but this text is wrapped, and does not hscroll
                    # else hscroll lost on rotation (kivy bug)
                    # on_height: root.enable_logs_text_hscroll()
                    #
                    #TextInputNoSelect:
                    #    id: helptext
                    #    text: root.help_message
                    #    multiline: True
                    #    readonly: True
                    #    allow_copy: False  # else odd select-all popup 
                    #    do_wrap: True
                    #    foreground_color: 'white'
                    #    background_color: '#0f2f2f'        # 'black' is hard to read
                    #    font_name: 'RobotoMono-Regular'    # default is hard to read
                    #    # padding: [42, 16]                # [h, v], mind the screen curves
                    #    padding: [dp(18), dp(8)]           # scale to screen density [1.1]
                    #
                    #    # to enable scrolling
                    #    size_hint: (None, None)
                    #    width: helptextscroll.width
                    #    height: max(self.minimum_height, helptextscroll.height)
                    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

               
                #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                # [1.5] Failed attempt: replacing the ScrollView above with the
                # following RecycleView cut memory use substantially when Help
                # and About are viewed (from ~850M to ~500M), but scrolling 
                # was too jerky to be usable.  Improving this may require fixed-
                # size items, and that seems impossible with wrapped-text Labels.
                #
                #RecycleView:
                #    id: helptextrview                 # .py sets .data with line strs
                #    viewclass: 'LabelTextDisplay'
                #
                #    RecycleBoxLayout:
                #        padding: [dp(10), dp(10)]     # pad top/bottom here, not RV or Lable
                #        do_scroll_x: False
                #        do_scroll_y: True
                #        effect_cls: 'ScrollEffect'    # don't overscroll/snapback
                #
                #        default_size: None, None      # val2 stops jumpy scrolls, may botch lines
                #        default_size_hint: 1, None
                #        size_hint_y: None
                #        height: self.minimum_height
                #        orientation: 'vertical'
                #
                # The .py code simply did this, though updates for fontsize 
                # changes were never tested or addressed:
                #
                #     # using N > 1 lines per Label did not help here: still jerky
                #     labelbox = self.ids.helptextrview
                #     lines = text.splitlines()                                   # 1 line/Label
                #     labelbox.data = [{'text': line or ' '} for line in lines]   # Label.text
                #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


                #GridLayout:
                #    cols: 2
                #    size_hint_y: 0.20

                #BoxLayout:
                #    size_hint_y: 0.08
                #    height: buttonheight


                DialogButtonsRows1:          # [1.5] uniform fixed size, culled, shortened

                    DialogButton:
                        text: 'Docs'
                        on_release: root.open_web_page('https://quixotely.com/PC-Phone USB Sync/User-Guide.html')

                    DialogButton:
                        text: 'Website'
                        on_release: root.open_web_page('https://quixotely.com/PC-Phone USB Sync/index.html')

                    DialogButton:
                        text: 'Backups'
                        on_release: root.do_explore_backups()


                    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                    # [1.5] retain temp
                    #DialogButton:
                    #    text: 'App Downloads'
                    #    on_release: root.open_web_page('https://quixotely.com/PC-Phone USB Sync/App-Packages.html')
                    #
                    #DialogButton:
                    #    text: 'Mergeall Website'
                    #    on_release: root.open_web_page('https://learning-python.com/mergeall.html')
                    #
                    #DialogButton:
                    #    text: 'Mergeall Docs'
                    #    on_release: root.open_web_page('https://learning-python.com/mergeall-products/unzipped/UserGuide.html')
                    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~




#------------------------------------------------------------------------------
# ABOUT TAB
#------------------------------------------------------------------------------


        TabbedPanelItem:
            id: abouttab
            text: 'About'

            BoxLayout:
                orientation: 'vertical'


                ScrollView:
                    # now required for spit-text labels box 
                    size_hint_y: 0.95
                    size_hint_x: 1.0
                    id: abouttextscroll
                    do_scroll_x: False
                    do_scroll_y: True
                    effect_cls: 'ScrollEffect'    # don't overscroll/snapback


                    # [1.5] use multiple Labels for text markup, and split 
                    # text to avoid blackouts.  See notes at Help above.

                    BoxLayout:
                        orientation: 'vertical'
                        size_hint_y: None
                        height: self.minimum_height

                        VerticalSpacer10:       # add padding at top: kivy 2.1 padding is 2 vals

                        SplitTextBoxLayout:     # main.py adds N LabelTextDisplay for split text
                            id: abouttextbox    # this precludes a common class

                        VerticalSpacer10:       # add padding at bottom: kivy 2.1 padding 2 vals


                    # [1.5] former scheme - see Help
                    #TextInputNoSelect:
                    #    foreground_color: 'white'
                    #    background_color: 'black'


                DialogButtonsRows1:          # [1.5] uniform fixed size, Mergeall from Help

                    DialogButton:
                        text: 'Play Store'
                        on_release: root.open_web_page('https://play.google.com/store/apps/details?id=com.quixotely.usbsync')

                    DialogButton
                        text: 'Downloads'
                        on_release: root.open_web_page('https://quixotely.com/PC-Phone USB Sync/App-Packages.html')

                    DialogButton:
                        text: 'Mergeall'
                        on_release: root.open_web_page('https://learning-python.com/mergeall.html')




#------------------------------------------------------------------------------
# MAIN-TAB FOLDER-CHOOSER POPUP
#------------------------------------------------------------------------------


<MainPathPickDialog>:

    # File chooser modal popup, for Main tab's FROM/TO paths
    # Pass oncanel, onpick, etc, and fill in storage buttons at top

    # [1.5] Hindsight: this coding nests a Boxlayout in a FloatLayout.  
    # The latter is the superclass of this <> class in the .py and is 
    # largely pointless: we could just use BoxLayout in the .py and 
    # skip an indentation level here.  OTOH, this allows configuring 
    # the BoxLayout here instead of in the .py; and if it ain't broke...

    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: 'vertical'


        ScrollView:

            # scroll horizontally for large fonts or names, and many devices
            # nit: may be moot on phones (>1 removable?), pc windows can expand
            # but usb hubs _do_ work on android, and large fonts are possible
            # nit: fixed-size child seems to preclude expanding into hspace,
            # but kivy doesn't have css's minwidth equiv (scroll at <= this);

            # that is, kivy seems to dictate scroll or expand, but not both;
            # in principle, might bind scrollbar size to a callback that calcs
            # normal texture-based size of buttons and hence boxlayout, and 
            # distributes excess evenly among buttons if scrollbar is wider;
            # but new layout seems better: space at end implies more options;  

            size_hint: (1, None)
            id: devicebuttonsscroll
            do_scroll_x: True              # auto opens at top of text
            effect_cls: 'ScrollEffect'     # don't overscroll/snapback

            height: tallbuttonheight       # a wee bit taller for taps
            width: root.width
        
            BoxLayout:

                # the .py adds variable number ToggleButton childen here;
                # note: dynamic is a whole lot harder with scrolling;
                # also catches tap of already down toggle button: goto root
                # note: catching down taps adds a full order of complexity,
                # because normal button events don't fire: see on_touch_down;

                id: devicebuttons
                orientation: 'horizontal'

                # all sizing/scrolling now done in .py do_main_path()
                # size_hint: (None, None)
                # height: devicebuttonsscroll.height
                # width: max(self.minimum_width, devicebuttonsscroll.width)
                # width: self.minimum_width  <= avoid triggering scroll too soon

                # UPDATE: boxlayout sizing is now here - simpler than in .py,
                # and works sans buttons (though buttons must be sized in .py
                # when built); in kivy, the docs are so thin that you have to 
                # do it the wrong way first - and a few times; please improve!

                size_hint: (None, 1)
                width: self.minimum_width     # set to min-width when computed


        BoxLayout:
            size_hint_y: None
            height: tallbuttonheight          # rarely used, but match others
            orientation: 'horizontal'

            # these are not radio buttons (group) because the down shading 
            # would be too distracting here;  their counterparts in the 
            # Configs tab are radios (and set by the on_releases below) 

            Button:
                text: 'Icon View'
                on_release: filechooser.view_mode = 'icon'; root.onviewmode('icon')

            Button:
                text: 'List View'
                on_release: filechooser.view_mode = 'list'; root.onviewmode('list')

            # [1.4] Punt: logic differs from icon/list here, and rarer - make a config only
            #Button:
            #    text: 'Hidden'
            #    on_release: filechooser.show_hidden = not filechooser.show_hidden   # nope
 

        PathDisplayNoSelect:
            id: pathname
            text: root.pickstart

            # and selection+handles+bubbles disabled in .py
            # PUNT: True disables scrolls, ScrollView hurts, and inputs goto Main 
            # readonly: True    # because changes have no effect on chooser

            size_hint: (1, None)
            height: self.minimum_height

            # width: max(self._get_row_width(0), pathnamescroll.width)
            # width: max(len(pathname.text), pathnamescroll.width)


        FileChooser:
            id: filechooser
            path: root.pickstart       # reset on device change (creation arg property)
            show_hidden: False         # [1.4] .py now sets on popup open from Config tab

            # per source code, mode names 'list' and 'icon' don't exist until 
            # after choosers below are built, but no code allowed after here (!$#);
            # set this only in the .py after popup built, and by buttons above
 
            # view_mode: root.viewmode   # set in .py by id, based on Config tab radio btns

            # limit navigation to the start|reset folder and below
            # removes '..' to avoid uncaught excs for folders sans access

            rootpath: root.rootpath     # reset on device change (creation arg property)
 
            # filter out files: show+select dirs only 
            # filters filters _in_, filter_dirs: False adds all dirs _in_
            # or filters: [lambda p, f: os.path.isdir(os.path.join(p, f))]
            # sort_func(x, y): default=folders first, uppercase first (macOS)

            dirselect: True                           # allow dir slections
            filter_dirs: False                        # add back all dirs post filters
            filters: [(lambda path, file: False)]     # remove all files+dirs at first

            # update text field on taps, fetch from there

            on_selection: pathname.text = self.selection and self.selection[0] or ''

            # view_mode: 'list'
            # choose one by magic buttons above, icons first per order;
            # font size in the first is fixed by a template copy/mod ahead here;
            # label "Size" in the second is nuked by children-lists crawl in .py;

            FileChooserIconLayout
                id: filechoosericonlayout

            FileChooserListLayout
                id: filechooserlistlayout    # for 'Size' nuke


        #BoxLayout:
        #    size_hint_y: None
        #    height: tallbuttonheight        # no need for tall at bottom, but match

        DialogButtonsRows1:                  # [1.5] uniform fixed size

            DialogButton:
                text: 'Cancel'
                on_release: root.oncancel(root)    # validate root=content instance

            DialogButton:
                text: 'Pick'
                on_release: root.onpick(pathname.text, root)




#------------------------------------------------------------------------------
# INFO POPUP
#------------------------------------------------------------------------------


<InfoDialog>:
  
    # General modal info popup: pass message, oncancel
    # Toast alternative (must also declare class in .py)
    # [1.5] BoxLayout in FloatLayout - see MainPathPickDialog

    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: 'vertical'

        # ([1.5] now moot, but retained temp for reference/context) 
        # because the TextInput itself would not scroll vertically;
        # text normally fits, and this uses default [do_wrap: True],
        # but add scrollview for large text or fontsize - textinput
        # won't vscroll by itself; scrolling is also much more likely 
        # to be needed in landscape mode on phones; caveat: this code 
        # is repeated redundantly: root.message won't work otherwise?

        ScrollView:
            size_hint: (1.0, 1.0)
            id: textscroll
            do_scroll_y: True                # auto opens at top of text
            effect_cls: 'ScrollEffect'       # don't overscroll/snapback

            SpacedLabelTextDisplay:          # [1.5] Label for colors and headers
                text: root.message           # with padding on top and bottom

            #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            #retain temp
            #TextInputNoSelect:
            #    text: root.message
            #    multiline: True
            #    readonly: True               # True: no keyboard on tap [1.1]
            #    foreground_color: 'white'
            #    background_color: 'black'
            #    # padding: [24, 24]          # [h, v], mind the screen curves
            #    padding: [dp(10), dp(10)]    # scale to screen density [1.1]
            # 
            #    # to enable scrolling
            #    size_hint: (None, None)
            #    width: textscroll.width
            #    height: max(self.minimum_height, textscroll.height)
            #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

        #BoxLayout:
        #    size_hint_y: None
        #    height: buttonheight

        DialogButtonsRows1:                        # [1.5] uniform fixed size

            DialogButton:
                text: 'Okay'
                on_release: root.oncancel(root)    # validate root=content instance




#------------------------------------------------------------------------------
# CONFIRM POPUP
#------------------------------------------------------------------------------


<ConfirmDialog>:

    # General modal verify popup: pass message, onyes, onno
    # [1.5] BoxLayout in FloatLayout - see MainPathPickDialog

    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: 'vertical'

        # see scrolling docs at InfoDialog above

        ScrollView:
            size_hint: (1.0, 1.0)
            id: textscroll
            do_scroll_y: True                  # auto opens at top of text
            effect_cls: 'ScrollEffect'         # don't overscroll/snapback

            SpacedLabelTextDisplay:            # [1.5] Label for colors and headers
                text: root.message             # with padding on top and bottom

            #TextInputNoSelect:                # former scheme - see InfoDialog

        #BoxLayout:
        #    size_hint_y: None
        #    height: buttonheight

        DialogButtonsRows1:                    # [1.5] uniform fixed size

            DialogButton:
                text: 'No'
                on_release: root.onno(root)    # validate root=content instance

            DialogButton:
                text: 'Yes'
                on_release: root.onyes(root)




#------------------------------------------------------------------------------
# COLOR POPUP
#------------------------------------------------------------------------------


<ColorPickDialog>:

    # Prebuilt content: pass oncancel, onpick
    # [1.5] BoxLayout in FloatLayout - see MainPathPickDialog

    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: 'vertical'

        ColorPicker
            id: colorpicker

        #BoxLayout:
        #    orientation: 'horizontal'
        #    size_hint_y: None
        #    height: buttonheight

        DialogButtonsRows1:                        # [1.5] uniform fixed size

            DialogButton:
                text: 'Cancel'
                on_release: root.oncancel(root)    # validate root=content instance

            DialogButton:
                text: 'Pick'
                on_release: root.onpick(colorpicker.color, colorpicker.hex_color, root)




#------------------------------------------------------------------------------
# TRIAL-ENDED POPUP
#------------------------------------------------------------------------------


<TrialEndedDialog>:

    # NO LONGER USED, as of [1.3]
    # After N opens in trial app: pass message, oncancel
    # This might be subverted, but it's not worth the struggles
    # [1.5] BoxLayout in FloatLayout - see MainPathPickDialog

    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: 'vertical'

        # see scrolling docs at InfoDialog above

        ScrollView:
            size_hint: (1.0, 1.0)
            id: textscroll
            do_scroll_y: True                # auto opens at top of text
            effect_cls: 'ScrollEffect'       # don't overscroll/snapback

            SpacedLabelTextDisplay:          # [1.5] Label for colors and headers
                text: root.message           # with padding on top and bottom

            #TextInputNoSelect:              # former scheme - see InfoDialog

        #BoxLayout:
        #    size_hint_y: None
        #    height: buttonheight

        DialogButtonsRows1:                  # [1.5] uniform fixed size

            DialogButton:
                # mind the app.root.method
                text: 'Get Full App'
                on_release: app.root.open_web_page('https://play.google.com/store/apps/details?id=com.quixotely.usbsync')

            DialogButton:
                text: 'Exit Trial App'
                on_release: root.oncancel()   # no root=content validate: stopping (moot)




#------------------------------------------------------------------------------
# NAME-MODE POPUP
#------------------------------------------------------------------------------


<ConfirmNameDialog>:
  
    # Confirm NAME action by a report|update mode choice
    # Pass message, onrunreport, onrunupdate, oncancel
    # [1.5] BoxLayout in FloatLayout - see MainPathPickDialog

    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: 'vertical'

        # see scrolling docs at InfoDialog above

        ScrollView:
            size_hint: (1.0, 1.0)
            id: textscroll
            do_scroll_y: True                # auto opens at top of text
            effect_cls: 'ScrollEffect'       # don't overscroll/snapback

            SpacedLabelTextDisplay:          # [1.5] Label for colors and headers
                text: root.message           # with padding on top and bottom

            #TextInputNoSelect:              # former scheme - see InfoDialog

        #BoxLayout:
        #    size_hint_y: None
        #    height: buttonheight

        DialogButtonsRows1:                        # [1.5] uniform fixed size

            DialogButton:
                text: 'Cancel'
                on_release: root.oncancel(root)    # validate root=content instance

            DialogButton:
                text: 'Run Report'
                on_release: root.onrunreport(root)

            DialogButton:
                text: 'Run Update'
                on_release: root.onrunupdate(root)




#------------------------------------------------------------------------------
# KIVY FILECHOOSER BUG WORKAROUND (for which this app is deeply ashamed...)
#------------------------------------------------------------------------------


# (NOTE: this might not be used in later Kivy, but is used in 2.1.0)
#
# The filechooser's icon view clips text vertically when fonts are 
# set larger than usual by the app or user settings.  This may be rare
# on phones, but is common elsewhere (e.g., Linux + high-res screens).
#
# There seems no way to fix this apart from the following.  Kivy loads
# its own "style.kv" file first from its intall's data/ folder, before 
# loading user .kv files (like this) or running Builder calls in the .py.
# The style.kv file defines styles for all Kivy builtin widgets, including
# the filechooser, which uses both list+icon views and a generic controller.
#
# To fix, copy the FileIconEntry template code in style.kv to here, and 
# mod the filename label to use size settings that make it as tall as needed
# for the text's 'texture' (font/scaling) size.  See the '##[ML' mods below.
# This avoids changing the built-in style.kv, but depends on its contents.
# Note that Label has texture, but TextInput has minumum_height (confusingy).
#
# This relies perilously on Kivy implementation details and is subpar, 
# but there is no other known option, and filename clips are a quite bad
# user experience (along with the ~dozen other Kivy bugs worked around).
# Why this sizing scheme isn't used in the filechooser is anybody's guess.
#
# In Kivy's defense, there usually ARE ways to work around such things 
# because it's all Python down to the graphics layer.  But its user base 
# might expand with a few crucial docs and fixes.  See also orange dots 
# left by double clicks on PCs; black boxes in TextInput for long lines;
# lag for loading text into TextInputs; hangs on app close on Android;
# builder battles; the .py's most horrible hack ever to kill the bogus
# "Size" label in FileChooserListLayout which is useless for folders and
# showed up just as an "e" for some phones and/or font sizes; plus all 
# the other bugs zapped here and in the .py (frustration happens).
#
# Hindsight: this may have been do-able with children-list crawls like 
# the list-layout "Size" fix in the .py, instead of this code copy+mod.
# OTOH, this works as is, and children lists seem just as brittle.
# This, and more, should really be exposed for mods via ids or API.
#
# References:
# https://github.com/kivy/kivy/blob/master/kivy/uix/filechooser.py
# https://github.com/kivy/kivy/blob/master/kivy/data/style.kv
# https://kivy.org/doc/stable/api-kivy.uix.label.html#sizing-and-text-content


# REDEF BUILT-IN WIDGET'S STYLE


[FileIconEntry@Widget]:
    locked: False
    path: ctx.path
    selected: self.path in ctx.controller().selection
    size_hint: None, None

    on_touch_down: self.collide_point(*args[1].pos) and ctx.controller().entry_touched(self, args[1])
    on_touch_up: self.collide_point(*args[1].pos) and ctx.controller().entry_released(self, args[1])
    size: '100dp', '100dp'

    canvas:
        Color:
            rgba: 1, 1, 1, 1 if self.selected else 0
            ##[ML test] rgba: 99, 99, 99, 99 if self.selected else 0
        BorderImage:
            border: 8, 8, 8, 8
            pos: root.pos
            size: root.size
            source: 'atlas://data/images/defaulttheme/filechooser_selected'

    Image:
        size: '48dp', '48dp'
        source: 'atlas://data/images/defaulttheme/filechooser_%s' % ('folder' if ctx.isdir else 'file')
        pos: root.x + dp(24), root.y + dp(40)
    Label:
        text: ctx.name
        font_name: ctx.controller().font_name
        ##[ML mod] text_size: (root.width, self.height)
        halign: 'center'
        shorten: True
        ##[ML mod] size: '100dp', '16dp'
        pos: root.x, root.y + dp(16)

        ##[ML add]
        text_size: root.width, None
        size: self.texture_size

    Label:
        text: '{}'.format(ctx.get_nice_size())
        font_name: ctx.controller().font_name
        font_size: '11sp'
        color: .8, .8, .8, 1
        size: '100dp', '16sp'
        pos: root.pos
        halign: 'center'






#******************************************************************************
# ONLY DEFUNCT CODE FOLLOWS
#******************************************************************************




#------------------------------------------------------------------------------
# NO LONGER USED: now embedded in Logs tab...
#------------------------------------------------------------------------------


#<LogfilePickDialog>:
#
#    # File chooser modal popup, for Logs tab's logfile path
#    # Use popup so always has surrent folder contents.
#    # Only show logfiles: no folder navigation required.
#
#    BoxLayout:
#        size: root.size
#        pos: root.pos
#        orientation: 'vertical'
#       
#        PathDisplay:
#            id: pathname
#            text: root.pickstart
#            readonly: True
#
#        FileChooser:
#            id: filechooser
#            path: root.pickstart
#            show_hidden: False
#            rootpath: root.pickstart    # drop '..' in picker
#
#            # show+select logfiles only, no dirs, listview only
#            dirselect: False
#            filter_dirs: True
#            filters: ['*date*-time*--*.txt']
#
#            on_selection: pathname.text = self.selection and self.selection[0] or ''
#            FileChooserListLayout
#
#        BoxLayout:
#            size_hint_y: None
#            height: buttonheight
#
#            DialogButton:
#                text: 'Cancel'
#                on_release: root.oncancel(root)
#
#            DialogButton:
#                text: 'Pick'
#                on_release: root.onpick(pathname.text, root)




#------------------------------------------------------------------------------
# NO LONGER USED: old configs and config-tab cruft, tmi...
#------------------------------------------------------------------------------


#SectionLabel:
#    size_hint_y: 0.10
#    text: 'Run Options'
#
#SectionLabel:
#    size_hint_y: 0.10
#    text: 'Appearance'
#
#Button:
#    text: 'Tip'
#    on_release: 
#        root.info_message('SYNC backups are in TO/__bkp__',
#        usetoast=True, longtime=True)


#Just always log, but make the folder self-cleanding to avoid space bloat
#BoxLayout:
#    orientation: 'horizontal'
#    CheckBox:
#        id: logfilescheckbox
#        active: True
#    Label:
#        text: 'Save Logfiles for Main-Tab Actions?'
                

#Show on Logs tab, as uneditable/unselectable text; else tmi...
#BoxLayout:
#    orientation: 'horizontal'
#    Label:
#        text: 'Logfiles folder:'
#    TextInput:
#        id: logfilesfolder


#Just test os.listdir and auto display ROOT if have access to "/"
#BoxLayout:
#    id: rootfolderbox
#    orientation: 'horizontal'
#    CheckBox:
#        id: rootfolderselections
#        active: False
#    Label:
#        text: 'Allow Root Folder Selections'


#Way too much: skip list + keep list, case, pattern syntax, module mods
#BoxLayout:
#    orientation: 'horizontal'
#    Label:
#        text: 'Cruft Names Pattern:'
#    TextInput:
#        id: cruftnamespattern


#Now a Main action: prestep too odd to wedge into thread-launch model 
#Label:
#    text: ''
#
#BoxLayout:
#    id: fixfilenamesbox
#    orientation: 'horizontal'
#    CheckBox:
#        id: fixfilenamescheckbox
#        active: True
#    Label:
#        text: 'Fix Nonportable Filenames in FROM?'